package temperature;

import java.math.BigDecimal;
import java.math.RoundingMode;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of temperatures involving a numeral and a temperature scale.
 * 
 * @Invar	The numeral of each temperature must be a valid numeral 
 * 			for the given scale.
 * 			| isValidNumeral(getNumeral(), getTemperatureScale())
 * @Invar	The temperature scale of each temperature must be a valid 
 * 			temperature scale.
 * 			| isValidTemperatureScale(getTemperatureScale())
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 */
@Value
public class Temperature implements Comparable<Temperature> {
	
	private int hashcode = 0;
	private static final int HASH_INITIALIZING_VALUE = 0x7ffffff; //Chosen because 2^30 - 1 initializes every hash bit except the first 2 as being 1
	private static final int HASH_MULTIPLICATION_VALUE = 524287; //Chosen as the 7th Mersenne prime, because we can, and also doesn't cause bit-shift

	/**
	 * Initialize this new temperature with given numeral and given scale.
	 * 
	 * @param 	numeral
	 * 			The numeral for this new temperature.
	 * @param 	temperatureScale
	 * 			The scale for this new temperature.
	 * @post	The numeral of this new temperature is the same as 
	 * 			the given numeral.
	 * 			| new.getNumeral() == numeral
	 * @post	The scale for this new temperature is the same as
	 * 			the given scale.
	 * 			| new.getTemperatureScale == temperatureScale
	 * @throws	IllegalArgumentException
	 * 			The given scale is not a valid scale.
	 * 			| ! isValidScale(temperatureScale)
	 * @throws	IllegalArgumentException
	 * 			The given numeral is not a valid numeral for 
	 * 			the given scale.
	 * 			| ! isValidNumeral(numeral, temperatureScale)
	 */
	@Raw
	public Temperature(BigDecimal numeral, TemperatureScale temperatureScale) 
		throws IllegalArgumentException {
		if(!isValidScale(temperatureScale))
			throw new IllegalArgumentException("Invalid temperature scale");
		if(!isValidNumeral(numeral, temperatureScale))
			throw new IllegalArgumentException("Invalid temperature for this scale");
		this.numeral = numeral;
		this.temperatureScale = temperatureScale;
	}

	/**
	 * Initialize this new temperature with given numeral and scale degree Celsius.
	 * 
	 * @param 	numeral
	 * 			The numeral for this new temperature.
	 * @effect 	This new temperature is initialized with the given numeral
	 * 			and the scale degree Celsius.
	 * 			| this(numeral, TemperatureScale.CELSIUS)
	 */
	@Raw
	public Temperature(BigDecimal numeral){
		this(numeral, TemperatureScale.CELSIUS);
	}

	/**
	 * Variable referencing a Temperature of 0 degrees Celsius.
	 * 
	 * @return	The temperature CELSIUS_0 is equal to a temperature 
	 * 			initialized with numeral BigDecimal.ZERO and with 
	 * 			scale TemperatureScale.CELSIUS.
	 * 			| CELSIUS_0.equals(new Temperature(BigDecimal.ZERO, TemperatureScale.CELSIUS))
	 */
	public static final Temperature CELSIUS_0 = 
		new Temperature(BigDecimal.ZERO, TemperatureScale.CELSIUS ); 
	
	/**
	 * Variable referencing a Temperature of -273.15 degrees Celsius.
	 * 
	 * @return	The temperature ABSOLUTE_ZERO is equal to a temperature 
	 * 			initialized with numeral BigDecimal.valueOf(-273.15) and with 
	 * 			scale TemperatureScale.CELSIUS.
	 * 			| ABSOLUTE_ZERO.equals(new Temperature(BigDecimal.valueOf(-273.15)
	 * 			|										,TemperatureScale.CELSIUS))
	 */
	public static final Temperature ABSOLUTE_ZERO = 
		new Temperature(BigDecimal.valueOf(-273.15), TemperatureScale.CELSIUS ); 
		
	//Other frequently used temperatures can be added here.
	
	
	/**
	 * Returns a BigDecimal that is the numeral of a temperature 
	 * that represents the absolute zero in the given temperature scale. 
	 * 
	 * @param 	temperatureScale
	 * 			The scale in witch the temperature must be expressed.
	 * @return	The resulting BigDecimal is effective.
	 * 			| result != null
	 * @return	The BigDecimal is equal to the numeral of a temperature 
	 * 			representing the absolute zero with the given scale.
	 * 			| switch (temperatureScale)
	 * 			| 	case CELSIUS: BigDecimal.valueOf(-273.15)
	 * 			|	case FAHRENHEIT: BigDecimal.valueOf(-459.67)
	 * 			| 	case KELVIN: BigDecimal.ZERO
	 * @throws	IllegalArgumentException
	 * 			The given temperature scale is not effective.
	 *  		| (temperatureScale != CELSIUS) && (temperatureScale != FAHRENHEIT) 
	 *  		|	&& (temperatureScale != KELVIN)
	 */
	public static BigDecimal getAbsoluteZero(TemperatureScale temperatureScale)
		throws IllegalArgumentException {
		if(temperatureScale == null) {
			throw new IllegalArgumentException("Invalid temperature scale");
		}
		switch (temperatureScale) {
		case CELSIUS :
			return BigDecimal.valueOf(-273.15);
		case FAHRENHEIT :
			return BigDecimal.valueOf(-459.67);	 
		case KELVIN :
			return BigDecimal.ZERO;
		default:
			throw new IllegalArgumentException("Invalid temperature scale");
		}
	}
	
	/**
	 * Return the numeral of this temperature.
	 */
	@Basic @Raw @Immutable
	public BigDecimal getNumeral() {
		return this.numeral;
	}
	
	/**
	 * Check whether the given numeral is a valid numeral for any temperature 
	 * with a given scale.
	 * 
	 * @param 	numeral
	 * 			The numeral to check.
	 * @param 	temperatureScale
	 * 			the scale in which the temperature is expressed.
	 * @return	True if and only if the numeral is effective 
	 * 			and if that numeral is bigger than absolute zero.
	 * 			| result == ( (numeral != null) 
	 * 			|	&& numeral.compareTo(getAbsoluteZero(temperatureScale)) != -1 )
	 * @throws	IllegalArgumentException
	 * 			The given scale is invalid.
	 * 			| !isValidScale(temperatureScale)
	 */
	public static boolean isValidNumeral(BigDecimal numeral,
			TemperatureScale temperatureScale) throws IllegalArgumentException {
		if(!isValidScale(temperatureScale))
			throw new IllegalArgumentException("Invalid temperature scale");
		if(numeral == null)
			return false;
		if( numeral.compareTo(getAbsoluteZero(temperatureScale)) == -1 )
			return false;
		return true;
	}

	/**
	 * Variable referencing the numeral for this temperature.
	 */
	private final BigDecimal numeral;
	
	
	/**
	 * Returns the scale of this temperature
	 */
	@Basic @Raw @Immutable
	public TemperatureScale getTemperatureScale() {
		return temperatureScale;
	}
	
	/**
	 * Check whether the given scale is a valid scale for any temperature.
	 * 
	 * @param 	temperatureScale
	 * 			The scale to check.
	 * @return	True if and only if the given scale is effective.
	 * 			| result == (temperatureScale != null)
	 */
	public static boolean isValidScale(TemperatureScale temperatureScale) {
		return temperatureScale != null;
	}
	
	/**
	 * Return a temperature that has the same value as this temperature 
	 * expressed in the given scale.
	 * 
	 * @param 	newScale
	 * 			The scale to convert to.
	 * @return	The resulting temperature is effective.
	 * 			| result != null
	 * @return	The resulting temperature has the given scale as its scale.
	 * 			| result.getTemperatureScale() == newScale
	 * @return	The numeral of the resulting temperature is equal to 
	 * 			the numeral of this temperature converted to the new scale.
	 * 			| switch (newScale)	
	 * 			| case CELSIUS: returnValue.toScale(this.getTemperatureScale()).getNumeral()
	 * 			|					.equals(this.getNumeral())
	 * 			| case FAHRENHEIT: returnValue.toScale(this.getTemperatureScale()).getNumeral()
	 * 			|						.equals(this.getNumeral())
	 * 			| case KELVIN: returnValue.toScale(this.getTemperatureScale()).getNumeral()
	 * 			|					.equals(this.getNumeral())
	 * @throws	IllegalArgumentException
	 * 			The given scale is invalid.
	 * 			| !isValidScale(temperatureScale)
	 */
	public Temperature toScale(TemperatureScale newScale) 
		throws IllegalArgumentException {
		if(!isValidScale(temperatureScale))
			throw new IllegalArgumentException("Invalid temperature scale");
		if (this.getTemperatureScale().equals(newScale))
			return this;
		Temperature temperatureCelsius = this.toCelsius();
		switch (newScale) {
			case CELSIUS :
				return temperatureCelsius;
			case FAHRENHEIT :
				return new Temperature( celsiusTofahrenheit(temperatureCelsius.getNumeral())
													,TemperatureScale.FAHRENHEIT );
			case KELVIN :
				return new Temperature( celsiusToKelvin(temperatureCelsius.getNumeral())
														,TemperatureScale.KELVIN );
			default:
				throw new IllegalArgumentException("Invalid temperature scale");
		}
	}
		
	/**
	 * Convert numeralCelsius form Celsius to Fahrenheit.
	 * 
	 * @param 	numeralCelsius
	 * 			The numeral in Celsius to convert to Fahrenheit.
	 * @return	The numeral converted from Celsius to Fahrenheit.
	 */
	private static BigDecimal celsiusTofahrenheit(BigDecimal numeralCelsius) {
		BigDecimal result;
		result = numeralCelsius.multiply(BigDecimal.valueOf(9)
					.divide(BigDecimal.valueOf(5), TEMPERATURE_PRECISION*2, RoundingMode.HALF_EVEN))
					.add(BigDecimal.valueOf(32));
				// x*9/5 + 32
		return result.setScale(TEMPERATURE_PRECISION*2, RoundingMode.HALF_EVEN);
	}

	/**
	 * Convert numeralCelsius form Celsius to Kelvin.
	 * 
	 * @param 	numeralCelsius
	 * 			The numeral in Celsius to convert to Kelvin.
	 * @return	The numeral converted from Celsius to Kelvin.
	 */
	private static BigDecimal celsiusToKelvin(BigDecimal numeralCelsius) {
		BigDecimal result;
		result = numeralCelsius.add(BigDecimal.valueOf(273.15));
				// x+273.15
		return result;
	}
	
	/**
	 * Return a temperature that has the same value as this temperature
	 * expressed in degree celsius.
	 * 
	 * @return	The resulting temperature is effective.
	 * 			| result != null
	 * @return	The resulting temperature has degree celsius.
	 * 			| result.getTemperatureScale() == CELSIUS
	 * @return	The numeral of the resulting temperature is equal to 
	 * 			the numeral of this temperature converted to celsius.
	 * 			| switch (this.getTemperatureScale())
	 * 			| case CELSIUS: returnValue.toScale(CELSIUS).getNumeral()
	 * 			|						.equals(this.getNumeral())
	 * 			| case FAHRENHEIT: returnValue.toScale(FAHRENHEIT)).getNumeral()
	 * 			|						.equals(this.getNumeral())
	 * 			| case KELVIN: returnValue.toScale(KELVIN).getNumeral()
	 * 			|						.equals(this.getNumeral())
	 * @throws 	IllegalStateException
	 * 			A temperature must have a valid scale.
	 * 			| !isValidScale(this.getTemperatureScale())
	 */
	public Temperature toCelsius() throws IllegalStateException{
		switch (this.getTemperatureScale()) {
			case CELSIUS :
				return this;
			case FAHRENHEIT :
				return new Temperature( fahrenheitToCelsius(this.getNumeral())
						,TemperatureScale.CELSIUS ); 
			case KELVIN :
				return new Temperature( kelvinToCelsius(this.getNumeral())
						,TemperatureScale.CELSIUS ); 
			default:
				throw new IllegalStateException();
		}
	}
	
	/**
	 * Convert numeralFahrenheit form Fahrenheit to Celsius.
	 * 
	 * @param 	numeralFahrenheit
	 * 			The numeral in Fahrenheit to convert to Celsius.
	 * @return	The numeral converted from Fahrenheit to Celsius.
	 */
	private static BigDecimal fahrenheitToCelsius(BigDecimal numeralFahrenheit) {
		BigDecimal result;
		result = numeralFahrenheit.subtract(BigDecimal.valueOf(32))
					.multiply(BigDecimal.valueOf(5)
					.divide(BigDecimal.valueOf(9), TEMPERATURE_PRECISION*2, RoundingMode.HALF_EVEN));
				// (x-32)*5/9
		return result.setScale(TEMPERATURE_PRECISION*2, RoundingMode.HALF_EVEN);
	}
	
	/**
	 * Convert numeralKelvin form Fahrenheit to Celsius.
	 * 
	 * @param 	numeralKelvin
	 * 			The numeral in Kelvin to convert to Celsius.
	 * @return	The numeral converted from Kelvin to Celsius.
	 */
	private static BigDecimal kelvinToCelsius(BigDecimal numeralKelvin){
		BigDecimal result;
		result = numeralKelvin.subtract(BigDecimal.valueOf(273.15));
		// x-273.15
		return result;		
	}
	
	/**
	 * Variable referencing the scale for this temperature.
	 */
	private final TemperatureScale temperatureScale;

	
	/**
	 * Variable referencing the scale of BigDecimals used in conversions 
	 * and comparisons.
	 */
	public static final int TEMPERATURE_PRECISION = 15;
	
	/**
	 * Variable referencing the precision at which temperatures will be output.
	 */
	public static final int FORMATTING_PRECISION = 3; 
	
	/** 
	 * Compare this temperature with the other temperature 
	 * with a precision of TEMPERATURE_PRECISION.
	 * 
	 * @param	other
	 * 			The other temperature to compare with.
	 * @return	The result is equal to the comparison of the numeral of 
	 * 			this temperature with the numeral of the other temperature 
	 * 			with a precision of TEMPERATURE_PRECISION.
	 * 			| let
	 * 			|	thisNumeral = getNumeral().setScale(TEMPERATURE_PRECISION,RoundingMode.HALF_EVEN)
	 * 			| and let
	 * 			|	otherNumeral = other.getNumeral().setScale(TEMPERATURE_PRECISION,RoundingMode.HALF_EVEN)
	 * 			| result == thisNumeral.compareTo(otherNumeral)
	 * @throws	ClassCastException
	 * 			The temperatures must be effective and must have the same scale.
	 * 			| ( (other == null) 
	 * 			|	|| (this.getTemperatureScale() != other.getTemperatureScale() )
	 */
	@Override
	public int compareTo(Temperature other) throws ClassCastException{
		if(other == null)
			throw new ClassCastException("Non-effective temperature");
		if(getTemperatureScale() != other.getTemperatureScale())
			throw new ClassCastException("Incompatible temperature scales");
		BigDecimal thisNumeral = getNumeral().setScale(TEMPERATURE_PRECISION,RoundingMode.HALF_EVEN);
		BigDecimal otherNumeral = other.getNumeral().setScale(TEMPERATURE_PRECISION,RoundingMode.HALF_EVEN);
		return thisNumeral.compareTo(otherNumeral);
	}
	
	/**
	 * Returns this temperature's textual representation formatted as:
	 * <temperature value> <symbol for the temperature scale>
	 * <temperature value> has a precision of FORMATTING_PRECISION.
	 * 
	 * @return 	A string representing the temperature formatted as text
	 * 			| getNumeral().toPlainString().setScale(FORMATTING_PRECISION, RoundingMode.HALF_EVEN) + " " + 
	 * 			|	getTemperatureScale().getSymbol()
	 */
	@Override
	public String toString(){
		int formattingScale = FORMATTING_PRECISION;
		BigDecimal numeral = getNumeral().setScale(formattingScale, RoundingMode.HALF_EVEN);
		String numeralString = ""+numeral;
		while(numeralString.substring(numeralString.length()-1 ,numeralString.length()).equalsIgnoreCase("0"))
		{
			numeralString = numeralString.substring(0,numeralString.length()-1);
		}
		if(numeralString.substring(numeralString.length()-1 ,numeralString.length()).equalsIgnoreCase("."))
			numeralString = numeralString.substring(0,numeralString.length()-1);
		return numeralString + " " + getTemperatureScale().getSymbol();
	}
	
	@Override
	public boolean equals(Object object){
		if(object == null)
			return false;
		if(this.getClass() != object.getClass())
			return false;
		Temperature other = (Temperature) object;
		return (this.getNumeral().setScale(FORMATTING_PRECISION, RoundingMode.HALF_EVEN)
				.equals(other.getNumeral().setScale(FORMATTING_PRECISION, RoundingMode.HALF_EVEN))) 
				&& (this.getTemperatureScale().equals(other.getTemperatureScale()));
	}
	
	@Override
	public int hashCode(){
		if(hashcode == 0){
			int hash = HASH_INITIALIZING_VALUE;
			for(int i = 0; i < 31 ; i++){
				hash = HASH_MULTIPLICATION_VALUE * hash + getNumeral().setScale(FORMATTING_PRECISION, RoundingMode.HALF_EVEN).hashCode();
				hash = HASH_MULTIPLICATION_VALUE * hash + getTemperatureScale().getSymbol().hashCode();
				hash += hash >> 2;
				hash += hash >> 3;
				hash += hash >> 5;
				hash += hash >> 7;
				hash += hash << 2;
				hash += hash << 3;
				hash += hash << 5;
				hash += hash << 7;
			}
			this.hashcode = hash;
		}
		return hashcode;
	}
}
